/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ir, native, HomeCheckIrHint } from 'qvog-dsl';
import { Defects, CheckerUtils, ScopeType, TempLocation, Scope } from '../../../Index';
import { StmtExt } from '../../../model/StmtExt';
import { IssueReport } from '../../../model/Defects';
import 'arkanalyzer/lib/core/base/Stmt';

const SEVERITY = 2;
const DOC_PATH = 'docs/constant-property-referencing-check-in-loops-dsl.md';
const DESCRIPTION = 'This property access occurs within a loop and returns a constant result; you are advised to extract it outside the loop body to reduce the number of property access times.';
let results: IssueReport[] = [];
let visited = new Set<string>();
let valueStr: string = '';
let indexStr: string = '';
let isArrayStaticField: boolean = false;
let isIndexConstant: boolean = true;
let isStaticFieldConstant: boolean = false;
let isRecursiveStart: boolean = false;
let recursiveTime: number = 0;
let defects: Defects[] = [];
let issues: IssueReport[] = [];
interface WarnInfo {
    line: number,
    startCol: number,
    endCol: number,
    filePath: string
}

export default native<HomeCheckIrHint>({ use: 'ir' })()
    .match((stmt): stmt is ir.Stmt => {
        const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
        const path = arkFile.getFilePath();
        if (!visited.has(path)) {
            results = [];
            visited.add(path);
            check(arkFile);
            return true;
        } else {
            return false;
        }
    })
    .report((_, { api }): void => {
        for (const result of results) {
            api.report({
                location: {
                    file: 'not implemented',
                    range: {
                        start: {
                            line: result.defect.reportLine,
                            column: result.defect.reportColumn
                        },
                        end: {
                            line: result.defect.reportLine,
                            column: result.defect.reportColumn
                        },
                    }
                },
                severity: SEVERITY,
                description: DESCRIPTION,
                docPath: DOC_PATH,
            })
        }
    });

function check(arkFile: ir.ArkFile): void {
    for (let clazz of arkFile.getClasses()) {
        constantCheckByArkMethods(clazz.getMethods());
    }
    for (let namespace of arkFile.getAllNamespacesUnderThisFile()) {
        for (let clazz of namespace.getClasses()) {
            constantCheckByArkMethods(clazz.getMethods());
        }
    }
};

function constantCheckByArkMethods(methods: ir.ArkMethod[]): void {
    let blocks: Set<ir.BasicBlock> | undefined = undefined;
    for (let method of methods) {
        blocks = method.getCfg()?.getBlocks();
        if (!blocks) {
            continue;
        }
        let blockValues = blocks.values();
        for (let i = 0; i < blocks.size; i++) {
            let value = blockValues.next().value;
            if (!value) {
                continue;
            }
            let stmts = value.getStmts();
            constantCheckByArkStmts(stmts, i, blocks, blockValues, method);
        }

    }
}

function constantCheckByArkStmts(stmts: ir.Stmt[], index: number, blocks: Set<ir.BasicBlock>,
    blockValues: IterableIterator<ir.BasicBlock>, method: ir.ArkMethod): void {
    for (let stmt of stmts) {
        if (!(stmt instanceof ir.ArkIfStmt)) {
            continue;
        }
        if (CheckerUtils.getScopeType(stmt) !== ScopeType.FOR_CONDITION_TYPE && CheckerUtils.getScopeType(stmt) !== ScopeType.WHILE_TYPE) {
            continue;
        }
        let variableMap: Map<string, number> = new Map<string, number>();
        let externalVariables: string[] = [];
        externalVariables = getExternalVariables(index, blocks);
        const value = blockValues.next().value;
        if (!value) {
            continue;
        }
        const cyclicStmt = value.stmts[0];
        if (!cyclicStmt) {
            continue;
        }
        const cyclicScope = (cyclicStmt as StmtExt).scope;
        index++;
        if (!cyclicScope) {
            continue;
        }

        let cyclicStmts = getCyclicStmts(cyclicScope, method);
        if (CheckerUtils.getScopeType(stmt) === ScopeType.FOR_CONDITION_TYPE) {
            variableMap = getVariableMap(externalVariables, cyclicStmts, cyclicScope);
            const conditionExpr = stmt.getConditionExpr();
            const op1 = conditionExpr.getOp1();
            setVariableMapByValue(op1, variableMap, stmt);
        }
        initData();
        constantCheckByCycliStmts(cyclicStmts, variableMap, '');
    }
}

function setVariableMapByValue(op1: ir.Value, variableMap: Map<string, number>, stmt: ir.Stmt): void {
    if (op1 instanceof ir.Local) {
        variableMap.set(op1.getName(), stmt.getOriginPositionInfo().getLineNo());
    }
}

function constantCheckByCycliStmts(stmts: ir.Stmt[], variableMap: Map<string, number>, operator: string): void {
    for (let stmt of stmts) {
        let leftName = '';
        if (stmt instanceof ir.ArkAssignStmt) {
            constantCheckByAssignStmt(leftName, stmt, variableMap, operator);
        } else if (stmt instanceof ir.ArkIfStmt) {
            checkByIfStmt(variableMap, operator, stmt);
            checkReportAndInit(leftName, stmt);
        }
    }
}

function constantCheckByAssignStmt(leftOpName: string, stmt: ir.ArkAssignStmt, variableMap: Map<string, number>, operator: string): void {
    let rightOp = stmt.getRightOp();
    let leftOp = stmt.getLeftOp();
    if (leftOp instanceof ir.Local) {
        leftOpName = leftOp.getName();
        if (leftOpName.startsWith('%') && !isRecursiveStart) {
            return;
        }
    }

    if (rightOp instanceof ir.ArkStaticFieldRef) {
        checkByStaticFieldRef(rightOp, variableMap, operator, stmt);
    } else if (rightOp instanceof ir.AbstractBinopExpr) {
        checkByBinopExpr(rightOp, variableMap, operator, stmt);
    } else if (rightOp instanceof ir.ArkArrayRef) {
        checkByArrayRef(rightOp, variableMap, operator);
    } else {
        dealWithElse(stmt);
    }

    checkReportAndInit(leftOpName, stmt);
}

function checkByStaticFieldRef(rightOp: ir.ArkStaticFieldRef, variableMap: Map<string, number>, operator: string, stmt: ir.Stmt): void {
    const staticFieldStr = getValueStr(rightOp);
    if (!variableMap.has(staticFieldStr)) {
        isStaticFieldConstant = true;
        if (isArrayStaticField) {
            if (indexStr === '') {
                indexStr = staticFieldStr;
            } else {
                indexStr = indexStr + ' ' + operator + ' ' + staticFieldStr;
            }
        } else {
            if (valueStr === '') {
                valueStr = staticFieldStr;
            } else {
                valueStr = valueStr + ' ' + operator + ' ' + staticFieldStr;
            }
        }
    } else {
        dealWithElse(stmt);
    }
}

function checkByBinopExpr(rightOp: ir.AbstractBinopExpr, variableMap: Map<string, number>, operator: string, stmt: ir.Stmt): void {
    const uses = rightOp.getUses();
    for (let use of uses) {
        if (!(use instanceof ir.Local)) {
            continue;
        }
        let useName = use.getName();
        if (useName.includes('%')) {
            const declaringStmt = use.getDeclaringStmt();
            if (declaringStmt && declaringStmt !== stmt) {
                operator = rightOp.getOperator();
                recursiveTime++;
                isRecursiveStart = true;
                constantCheckByCycliStmts([declaringStmt], variableMap, operator);
                recursiveTime--;
            }
        } else if (!variableMap.has(useName)) {
            withoutVariableMapOperation(useName, operator);
        } else {
            dealWithElse(stmt);
        }
    }
}

function checkByArrayRef(rightOp: ir.ArkArrayRef, variableMap: Map<string, number>, operator: string): void {
    isArrayStaticField = false;
    let arrayValueStr = '';
    const rightOpBase = rightOp.getBase();
    const declaringStmt = rightOpBase.getDeclaringStmt();
    if (!(declaringStmt instanceof ir.ArkAssignStmt)) {
        return;
    }
    const declaringStmtRightOp = declaringStmt.getRightOp();
    if (declaringStmtRightOp instanceof ir.ArkStaticFieldRef) {
        arrayValueStr = getValueStr(declaringStmtRightOp);
        isArrayStaticField = arrayValueStr !== '';
    }

    if (isArrayStaticField) {
        const index = rightOp.getIndex() as ir.Local;
        const indexName = index.getName();
        if (indexName.startsWith('%')) {
            handleRecursiveIndex(index, variableMap, operator);
        } else {
            handleNonRecursiveIndex(indexName, variableMap, operator);
        }

        if (indexStr.length > 0) {
            arrayValueStr = isIndexConstant ? arrayValueStr + '[' + indexStr + ']' : indexStr;
        }
        valueStr = valueStr ? valueStr + ' ' + operator + ' ' + arrayValueStr : arrayValueStr;
    }
}

function handleRecursiveIndex(index: ir.Local, variableMap: Map<string, number>, operator: string): void {
    const indexDeclaringStmt = index.getDeclaringStmt();
    if (indexDeclaringStmt) {
        recursiveTime++;
        isRecursiveStart = true;
        constantCheckByCycliStmts([indexDeclaringStmt], variableMap, operator);
        recursiveTime--;
    }
}

function handleNonRecursiveIndex(indexName: string, variableMap: Map<string, number>, operator: string): void {
    if (!variableMap.has(indexName)) {
        if (isArrayStaticField) {
            isStaticFieldConstant = true;
        }
        updateIndexStr(indexName, operator);
    }
}

function updateIndexStr(indexName: string, operator: string): void {
    if (indexStr === '') {
        indexStr = indexName;
    } else {
        indexStr = indexStr + ' ' + operator + ' ' + indexName;
    }
}

function checkByIfStmt(variableMap: Map<string, number>, operator: string, stmt: ir.ArkIfStmt): void {
    const conditionExpr = stmt.getConditionExpr();
    const op1 = conditionExpr.getOp1();
    if (op1 instanceof ir.Local) {
        const op1Name = op1.getName();
        if (op1Name.startsWith('%')) {
            const declaringStmt = op1.getDeclaringStmt();
            if (declaringStmt) {
                recursiveTime++;
                isRecursiveStart = true;
                constantCheckByCycliStmts([declaringStmt], variableMap, operator);
                recursiveTime--;
            }
        }
    }
}

function checkReportAndInit(leftName: string, stmt: ir.Stmt): void {
    if (isStaticFieldConstant && ((!leftName.startsWith('%') && !isRecursiveStart) ||
        (isRecursiveStart && recursiveTime === 0))) {
        pushIssueReports(valueStr, stmt);
    }
    if (recursiveTime === 0) {
        initData();
    }
}

function initData(): void {
    valueStr = '';
    indexStr = '';
    isStaticFieldConstant = false;
    isRecursiveStart = false;
    isArrayStaticField = false;
    isIndexConstant = true;
}

function dealWithElse(stmt: ir.Stmt): void {
    if (isStaticFieldConstant && isRecursiveStart && valueStr !== '') {
        pushIssueReports(valueStr, stmt);
    }
    if (isArrayStaticField && isRecursiveStart) {
        isIndexConstant = true;
    }
    if (valueStr !== '') {
        valueStr = '';
    }
}

function withoutVariableMapOperation(useName: string, operator: string): void {
    if (isArrayStaticField) {
        if (indexStr === '') {
            indexStr = useName;
        } else {
            indexStr = indexStr + ' ' + operator + ' ' + useName;
        }
    } else {
        if (valueStr === '') {
            valueStr = useName;
        } else {
            valueStr = valueStr + ' ' + operator + ' ' + useName;
        }
    }
}

function getValueStr(rightOp: ir.Value): string {
    let valueString = '';
    if (rightOp instanceof ir.ArkStaticFieldRef) {
        const classSignature = rightOp.getFieldSignature().getDeclaringSignature() as ir.ClassSignature;
        const className = classSignature.getClassName();
        const fieldName = rightOp.getFieldName();
        valueString = className + '.' + fieldName;
        const declaringNamespaceSignature = classSignature.getDeclaringNamespaceSignature();
        if (declaringNamespaceSignature) {
            const nameSpace = declaringNamespaceSignature.getNamespaceName();
            valueString = nameSpace + '.' + valueString;
        }
    } else if (rightOp instanceof ir.Local) {
        valueString = rightOp.getName();
    }
    return valueString;
}

function getVariableMap(externalVariables: string[], stmts: ir.Stmt[], scope: Scope): Map<string, number> {
    let variableMap: Map<string, number> = new Map();
    for (let stmt of stmts) {
        if (stmt instanceof ir.ArkAssignStmt) {
            let leftOp = stmt.getLeftOp();
            if (leftOp instanceof ir.Local) {
                getVariableMapByLocal(leftOp, externalVariables, stmt, variableMap);
            } else if (leftOp instanceof ir.ArkStaticFieldRef) {
                let classSignature = leftOp.getFieldSignature().getDeclaringSignature() as ir.ClassSignature;
                let className = classSignature.getClassName();
                let fieldName = leftOp.getFieldName();
                let staticFieldStr = className + '.' + fieldName;
                variableMap.set(staticFieldStr, stmt.getOriginPositionInfo().getLineNo());
            }
        }
    }
    const defList = scope.defList;
    for (let defItem of defList) {
        const redefInfoSize = defItem.redefInfo.size;
        if (redefInfoSize > 0) {
            variableMap.set(defItem.getName(), defItem.defStmt.getOriginPositionInfo().getLineNo());
        }
    }
    return variableMap;
}

function getVariableMapByLocal(leftOp: ir.Local, externalVariables: string[], stmt: ir.Stmt, variableMap: Map<string, number>): void {
    let leftOpName = leftOp.getName();
    for (let externalVariable of externalVariables) {
        if (leftOpName === externalVariable) {
            if (CheckerUtils.isDeclaringStmt(leftOpName, stmt)) {
                let index = externalVariables.indexOf(externalVariable);
                externalVariables.splice(index, 1);
            } else {
                variableMap.set(leftOpName, stmt.getOriginPositionInfo().getLineNo());
            }
        }
    }
}

function getCyclicStmts(scope: Scope, method: ir.ArkMethod): ir.Stmt[] {
    let stmts: ir.Stmt[] = [];
    let blocks = scope.blocks;
    let baseBlocks: ir.BasicBlock[] = [];
    for (let block of blocks) {
        baseBlocks.push(block);
    }
    let startStmt = baseBlocks[0].getStmts()[0];
    let lastBlockStmts = baseBlocks[baseBlocks.length - 1].getStmts();
    let endStmt = lastBlockStmts[lastBlockStmts.length - 1];
    let isAdd = false;
    let methodStmts = method.getCfg()?.getStmts();
    if (methodStmts) {
        for (let stmt of methodStmts) {
            if (stmt === startStmt) {
                isAdd = true;
            } else if (stmt === endStmt && isAdd) {
                stmts.push(stmt);
                break;
            }
            if (isAdd) {
                stmts.push(stmt);
            }
        }
    }
    return stmts;
}

function getExternalVariables(index: number, blocks: Set<ir.BasicBlock>): string[] {
    let externalVariables: string[] = [];
    let blockValues = blocks.values();
    for (let i = 0; i <= index; i++) {
        if (i === index) {
            break;
        }
        let stmts = blockValues.next().value?.getStmts();
        if (!stmts) {
            continue;
        }
        for (let stmt of stmts) {
            let externalName = getDeclaringName(stmt);
            if (externalName !== '' && !isExist(externalName, externalVariables)) {
                externalVariables.push(externalName);
            }
        }
    }
    return externalVariables;
}

function isExist(checkData: number | string, checkDatas: number[] | string[]): boolean {
    for (let data of checkDatas) {
        if (data === checkData) {
            return true;
        }
    }
    return false;
}

function getDeclaringName(stmt: ir.Stmt): string {
    let def = stmt.getDef();
    if (def instanceof ir.Local) {
        if (def.getName() === 'this') {
            return '';
        }
        if (CheckerUtils.wherIsTemp(stmt) === TempLocation.LEFT || CheckerUtils.isDeclaringStmt(def.getName(), stmt)) {
            return def.getName();
        }
    }
    return '';
}

function isExistIssueReport(defect: IssueReport): boolean {
    for (let issue of issues) {
        if (defect.defect.fixKey === issue.defect.fixKey && defect.defect.mergeKey === issue.defect.mergeKey) {
            return true;
        }
    }
    return false;
}

function pushIssueReports(checkText: string, stmt: ir.Stmt): void {
    let warnInfo = getWarnInfo(checkText, stmt);
    if ((warnInfo.filePath === '' && warnInfo.line === -1)) {
        initData();
        return;
    }
    const severity = SEVERITY;
    let defect = new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol, DESCRIPTION, severity, '', warnInfo.filePath,
        DOC_PATH, true, false, false);
    if (isExistIssueReport(new IssueReport(defect, undefined))) {
        initData();
        return;
    }
    results.push(new IssueReport(defect, undefined));
    initData();
}

function getWarnInfo(checkText: string, stmt: ir.Stmt): WarnInfo {
    const text = stmt.getOriginalText();
    const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
    if (!arkFile || !text || text.length === 0) {
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }
    let lineCount = - 1;
    let startColum = - 1;
    let originalPosition = stmt.getOriginPositionInfo();
    const sparse = originalPosition.getColNo();
    const originalTexts = text.split('\n');
    for (let originalText of originalTexts) {
        lineCount++;
        if (originalText.includes(checkText)) {
            if (lineCount === 0) {
                startColum = originalText.indexOf(checkText) + sparse;
            } else {
                startColum = originalText.indexOf(checkText) + 1;
            }
            break;
        }
    }
    if (startColum === -1) {
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }
    let lineNo = originalPosition.getLineNo() + lineCount;
    const endColumn = startColum + checkText.length - 1;
    const filePath = arkFile.getFilePath();
    return { line: lineNo, startCol: startColum, endCol: endColumn, filePath: filePath };
}
